#--
# Active Directory Module for Ruby
#
# Copyright (c) 2005-2006 Justin Mecham
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to
# deal in the Software without restriction, including without limitation the
# rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
# FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
# IN THE SOFTWARE.
#++

module ActiveDirectory

  #
  # Represents a User object within an Active Directory instance.
  #
  class User < Base

    # Distinguished Name (DN)
    attr_reader :dn

    # Display Name (e.g. "John Q. Public")
    attr_reader :name

    # Given Name (e.g. "John")
    attr_reader :given_name

    # Surname (e.g. "Public")
    attr_reader :surname

    # Primary E-Mail Address
    attr_reader :email

    # Account/Username (e.g. "jpublic")
    attr_reader :username

    # Job Title
    attr_reader :title

    # Company Name
    attr_reader :company

    # Department Name
    attr_reader :department

    # Primary Phone Number
    attr_reader :main_number

    # Mobile Number
    attr_reader :mobile_number

    # Street Address
    attr_reader :street_address

    # City / Town
    attr_reader :city

    # State/Province
    attr_reader :state

    # Zip/Postal Code
    attr_reader :zip

    # Country
    attr_reader :country

    # Direct Reports
    attr :direct_reports

    # Manager
    attr :manager

    # Groups this User is a member of
    attr :groups

    #
    # Attributes that we wish to pull from Active Directory for any User that
    # can be located within the directory.
    #
    ATTRIBUTES = ["displayName",        # Name (e.g. "John Doe")
                  "givenName",          # Given (First) Name
                  "sn",                 # Surname (Last)
                  "distinguishedName",  # DN of User
                  "sAMAccountName",     # Account Name
                  "mail",               # Primary E-Mail Address
                  "manager",            # DN Reference to Manager
                  "directReports",      # DN References to Minions
                  "memberOf",           # Group Membership
                  "company",            # Company Name
                  "department",         # Department Name
                  "title",              # Title
                  "mobile",             # Mobile Phone Number
                  "telephoneNumber",    # Primary Phone Number
                  "streetAddress",      # Street Address
                  "l",                  # City
                  "st",                 # State
                  "postalCode",         # Zip Code
                  "co"] #:nodoc:        # Country

    #
    # Attempts to load a User by a Distinguished Name (DN) or sAMAccountName.
    #
    def initialize(identifier)

      if (identifier =~ /(CN|cn)=/) != nil
        load_by_dn(identifier)
      else
        load_by_username(identifier)
      end

    end

    #
    # Attempts to authenticate the loaded user with the supplied password.
    # Returns true if the authentication attempt was successful.
    #
    def authenticate(password)

      # Clean up the password before we run it through our series of tests.
      password.strip!

      # If no password was specified, raise an exception. This check must
      # occur to avoid a huge security hole if anonymous bind is on - if this
      # check is not performed, someone can authenticate without providing a
      # password when anonymous bind is turned on.
      raise PasswordInvalid unless (!password.nil? and password.length > 0)

      # Clone our shared connection for isolated use in determining the
      # validity of our user's credentials.
      auth_connection = Base.connection.clone

      # Unbind the connection if it is already bound.
      auth_connection.unbind if auth_connection.bound?

      begin

        # Attempt to bind to the connection as the currently loaded user with
        # the supplied password.
        auth_connection.bind("#{@username}@#{@@server_settings[:domain]}",
                             password)

        return true

      rescue LDAP::ResultError
        if ($!.to_s == "Invalid credentials")
          raise PasswordInvalid
        else
          raise
        end
      ensure
        auth_connection.unbind
        auth_connection = nil
      end

      return false

    end

    #
    # Conveniently return the name of the User if the object is called
    # directly.
    #
    def to_s #:nodoc:
      @name
    end

    #
    # Proxy for loading and returning this users' manager.
    #
    def manager #:nodoc:
      @manager ||= User.new(@manager_dn) unless @manager_dn.nil?
    end

    #
    # Proxy for loading and returning the group membership of this user.
    #
    def groups #:nodoc:
      groups = Array.new
      unless @groups.nil?
        for group_dn in @groups
          groups << Group.new(group_dn)
        end
      end
      groups
    end

    #
    # Proxy for loading and returning the users who report directly to this
    # user.
    #
    def direct_reports #:nodoc:
      direct_reports = Array.new
      unless @direct_reports.nil?
        for user_dn in @direct_reports
          direct_reports << User.new(user_dn)
        end
      end
      direct_reports
    end

    #
    # Determines if the user is a member of the given group. Returns true if
    # the user is in the passed group.
    #
    def member_of?(group)
      @groups.include?(group.dn)
    rescue
      false
    end

    private

    #
    # Attempt to load a user by their username (sAMAccountName).
    #
    def load_by_username(username)

      if username.nil? or username.length == 0
        raise ArgumentError, "No username provided."
      end

      @username = username

      # Set our filter to include only information about the requested user of
      # class user.
      filter = "(&(objectClass=user)(sAMAccountName=#{@username}))"

      entries = Base.search(@@server_settings[:base_dn],
                  LDAP::LDAP_SCOPE_SUBTREE, filter, ATTRIBUTES)

      raise UnknownUserError if (entries.nil? or entries.length != 1)

      parse_ldap_entry(entries[0])

    end

    #
    # Attempt to load a User by its Distinguished Name (DN).
    #
    def load_by_dn(dn)

      if dn.nil? or dn.length == 0
        raise ArgumentError, "No distinguished name provided."
      end

      # De-escape the DN
      dn = dn.gsub(/"/, "\\\"")

      # Set our filter to include only information about the requested user of
      # class user.
      filter = "(&(objectClass=user))"

      entries = Base.search(dn, LDAP::LDAP_SCOPE_BASE, filter)

      raise UnknownUserError if (entries.nil? or entries.length != 1)

      parse_ldap_entry(entries[0])

    end

    def parse_ldap_entry(entry)
      @dn             = entry['distinguishedName'][0]
      @username       = entry['sAMAccountName'][0]
      @name           = entry['displayName'][0] unless entry['displayName'].nil?
      @given_name     = entry['givenName'][0] unless entry['givenName'].nil?
      @surname        = entry['sn'][0] unless entry['sn'].nil?
      @email          = entry['mail'][0] unless entry['mail'].nil?
      @manager_dn     = entry['manager'][0] unless entry['manager'].nil?
      @company        = entry['company'][0] unless entry['company'].nil?
      @department     = entry['department'][0] unless entry['department'].nil?
      @title          = entry['title'][0] unless entry['title'].nil?
      @main_number    = entry['telephoneNumber'][0] unless entry['telephoneNumber'].nil?
      @mobile_number  = entry['mobile'][0] unless entry['mobile'].nil?
      @street_address = entry['streetAddress'][0] unless entry['streetAddress'].nil?
      @city           = entry['l'][0] unless entry['l'].nil?
      @state          = entry['st'][0] unless entry['st'].nil?
      @zip            = entry['postalCode'][0] unless entry['postalCode'].nil?
      @country        = entry['co'][0] unless entry['co'].nil?
      @direct_reports = entry['directReports']
      @groups         = entry['memberOf']
    end

  end

end